TypeScript 泛型在您调用它时起作用,但在函数体中不起作用

您所在的位置:网站首页 lua 教程 TypeScript 泛型在您调用它时起作用,但在函数体中不起作用

TypeScript 泛型在您调用它时起作用,但在函数体中不起作用

2022-12-29 18:33| 来源: 网络整理| 查看: 265

First let's dispense with your conditional type definition of Values. This can be easily refactored to an indexed access into a mapping interface:

interface KeyVals { one: { id: 1, name: "hello1" }, two: { id: 2, value: "hello2" } } type Keys = keyof KeyVals; type Values = KeyVals[K];

The compiler is slightly better at analyzing indexed accesses than it at analyzing conditional types. This doesn't fix your problem, but it does make my workaround behave better, so I'll do that.

The main problem here is the mismatch between how the compiler deals with discriminated unions and how it deals with generics. Your fn() function call signature is generic, with values k of type K and v of type Values, but your function implementation needs to treat these as discriminated unions, where checking the value of k should narrow the type of v. But checking k only narrows the type of k, and does narrow the type parameter K. So you can't treat these generic values as discriminated unions.

This is sort of a general limitation in TypeScript currently, with various issues in GitHub raised about different aspects of it. For example, there's microsoft/TypeScript#33014, which explicitly asks for a check on k to narrow the type parameter K. That issue is currently open. It would probably also need microsoft/TypeScript#27808 to be implemented, so that you can prevent people from passing in a union for K, since that messes things up. There's also microsoft/TypeScript#30581 which asks for how to treat unions generically, and the recommended fix/approach to it at microsoft/TypeScript#47109 is to essentially reframe unions as generics... but here you need the union behavior to work. Right now this is just not possible, it seems.

You also tried the approach of making fn() a non-generic overloaded function. This would avoid generics entirely. Unfortunately the compiler is even less able to analyze overloads than it is with generics. Inside the implementation of an overloaded function statement the compiler does not constrain things to only be compatible with the call signatures; it only looks at the implementation signature, and it matches the implementation and call signatures very loosely. See microsoft/TypeScript#13235 for a declined request to have overloads checked in a type safe way. And if you use an overloaded function expression then the compiler is too strict. See microsoft/TypeScript#47669 for a still-open request to do something different here. So overloads can either be made to suppress errors (but allow some unsafe things) or catch errors (but prevent some safe things) but not both.

So, as far as I know, there's no way to refactor your code so that the compiler allows you to do the right thing while stopping you from doing the wrong thing. All I can think of is a workaround.

In this case, the closest I can get to useful safety is to use a function overload statement to bridge the gap between generics and discriminated unions. It looks like this:

function uncurriedFn(k: K, v: Values): void; function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values] }[Keys]) { if (k === 'one') { const name = v.name; } }

So uncurriedFn() is a single function that takes both k and v instead of the curried version that takes k and returns a function that takes v. The call signature of the function is generic, so you should be able to call it safely as a generic function. In fact, that lets you use it inside your intended curried version:

type Fn = (k: K) => (v: Values) => void; const fn: Fn = (k) => (v) => uncurriedFn(k, v) fn('one')({ id: 1, name: 'hello1' }); fn('two')({ id: 2, value: 'hello2' });

So that's the call signature. What about the implementation? Well, the implementation signature is non-generic and takes a destructured discriminanted union argument. The type {[K in Keys]: [k: K, v: Values]}[Keys] is a distributive object (as coined in ms/TS#47109) that evaluates to [k: 'one', v: Values] | [k: 'two', v: Values]. This also only allows safe calls; it will complain if you mismatch k and v. But now the implementation body can proceed to use k and v as a destructured discriminated union; check k, and v is narrowed.

And there you go. Everything is, in fact, safe (or as safe as reasonable... generics can be used unsafely sometimes), and the compiler doesn't complain. The trick is, again, this pair of call and implementation signatures:

function uncurriedFn(k: K, v: Values): void; function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values] }[Keys]) {}

In an ideal world the compiler would know that the signature (k: K, v: Values) => void and (...args: {[K in Keys]: [k: K, v: Values]}[Keys]) => void are compatible (modulo ms/TS#27808). But it doesn't know this, so you have to trick it by taking advantage of the looseness of overload statement checking.

Playground link to code



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3